/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.engine.impl.scripting;

import com.je.bpm.engine.ActivitiException;
import com.je.bpm.engine.delegate.VariableScope;
import javax.script.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ScriptingEngines {

    public static final String DEFAULT_SCRIPTING_LANGUAGE = "juel";
    public static final String GROOVY_SCRIPTING_LANGUAGE = "groovy";

    private final ScriptEngineManager scriptEngineManager;
    protected ScriptBindingsFactory scriptBindingsFactory;

    protected boolean cacheScriptingEngines = true;
    protected Map<String, ScriptEngine> cachedEngines;

    public ScriptingEngines(ScriptBindingsFactory scriptBindingsFactory) {
        this(new ScriptEngineManager());
        this.scriptBindingsFactory = scriptBindingsFactory;
    }

    public ScriptingEngines(ScriptEngineManager scriptEngineManager) {
        this.scriptEngineManager = scriptEngineManager;
        cachedEngines = new HashMap<String, ScriptEngine>();
    }

    public ScriptingEngines addScriptEngineFactory(ScriptEngineFactory scriptEngineFactory) {
        scriptEngineManager.registerEngineName(scriptEngineFactory.getEngineName(), scriptEngineFactory);
        return this;
    }

    public void setScriptEngineFactories(List<ScriptEngineFactory> scriptEngineFactories) {
        if (scriptEngineFactories != null) {
            for (ScriptEngineFactory scriptEngineFactory : scriptEngineFactories) {
                scriptEngineManager.registerEngineName(scriptEngineFactory.getEngineName(), scriptEngineFactory);
            }
        }
    }

    public Object evaluate(String script, String language, VariableScope variableScope) {
        return evaluate(script, language, createBindings(variableScope));
    }

    public Object evaluate(String script, String language, VariableScope variableScope, boolean storeScriptVariables) {
        return evaluate(script, language, createBindings(variableScope, storeScriptVariables));
    }

    public void setCacheScriptingEngines(boolean cacheScriptingEngines) {
        this.cacheScriptingEngines = cacheScriptingEngines;
    }

    public boolean isCacheScriptingEngines() {
        return cacheScriptingEngines;
    }

    protected Object evaluate(String script, String language, Bindings bindings) {
        ScriptEngine scriptEngine = getEngineByName(language);
        try {
            return scriptEngine.eval(script, bindings);
        } catch (ScriptException e) {
            throw new ActivitiException("problem evaluating script: " + e.getMessage(), e);
        }
    }

    protected ScriptEngine getEngineByName(String language) {
        ScriptEngine scriptEngine = null;

        if (cacheScriptingEngines) {
            scriptEngine = cachedEngines.get(language);
            if (scriptEngine == null) {
                scriptEngine = scriptEngineManager.getEngineByName(language);

                if (scriptEngine != null) {
                    // ACT-1858: Special handling for groovy engine regarding GC
                    if (GROOVY_SCRIPTING_LANGUAGE.equals(language)) {
                        try {
                            scriptEngine.getContext().setAttribute("#jsr223.groovy.engine.keep.globals", "weak", ScriptContext.ENGINE_SCOPE);
                        } catch (Exception ignore) {
                            // ignore this, in case engine doesn't support the
                            // passed attribute
                        }
                    }

                    // Check if script-engine allows caching, using "THREADING"
                    // parameter as defined in spec
                    Object threadingParameter = scriptEngine.getFactory().getParameter("THREADING");
                    if (threadingParameter != null) {
                        // Add engine to cache as any non-null result from the
                        // threading-parameter indicates at least MT-access
                        cachedEngines.put(language, scriptEngine);
                    }
                }
            }
        } else {
            scriptEngine = scriptEngineManager.getEngineByName(language);
        }

        if (scriptEngine == null) {
            throw new ActivitiException("Can't find scripting engine for '" + language + "'");
        }
        return scriptEngine;
    }

    /**
     * override to build a spring aware ScriptingEngines
     */
    protected Bindings createBindings(VariableScope variableScope) {
        return scriptBindingsFactory.createBindings(variableScope);
    }

    /**
     * override to build a spring aware ScriptingEngines
     */
    protected Bindings createBindings(VariableScope variableScope, boolean storeScriptVariables) {
        return scriptBindingsFactory.createBindings(variableScope, storeScriptVariables);
    }

    public ScriptBindingsFactory getScriptBindingsFactory() {
        return scriptBindingsFactory;
    }

    public void setScriptBindingsFactory(ScriptBindingsFactory scriptBindingsFactory) {
        this.scriptBindingsFactory = scriptBindingsFactory;
    }
}
